﻿using System;
using System.Collections.Generic;
using System.Text;

namespace com.bodurov.Html.HtmlSummarizer
{
    /*
    Blog: http://blog.bodurov.com/Html-Summarizer
    */
    public class Summarizer : ISummarizer
    {
        public static readonly HashSet<char> AsciiLetters = new HashSet<char> { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
        public static readonly HashSet<char> AsciiLettersAndNumbers = new HashSet<char> { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
        public static readonly HashSet<char> Spaces = new HashSet<char> { ' ', '\t', '\r', '\n', '\b' };
        public static readonly HashSet<char> SentanceLimiters = new HashSet<char> { '.', '!', '?', ':', ';' };
        public static readonly HashSet<string> AutoCloseTags = new HashSet<string> { "img", "hr", "br" };

        public Func<IStringIterationState, bool> SummaryEnds { get; set; }


        public string GetHtmlSummary(string completeHtml, int numberParts, PartType partType)
        {
            return this.GetHtmlSummary(completeHtml, s => 0, numberParts, partType);
        }

        public string GetHtmlSummary(string completeHtml, Func<string, int> defuneStartIndex, int numberParts, PartType partType)
        {
            if (String.IsNullOrEmpty(completeHtml))
            {
                return completeHtml;
            }
            completeHtml = completeHtml.Trim();

            var state = new StringIterationState
            {
                NumberParts = numberParts,
                PartType = partType
            };

            var onSummaryEnds = this.SummaryEnds ?? DefaultSummaryEndsFunc;

            var index = defuneStartIndex(completeHtml);

            if (index < 0) index = 0;

            for (var i = index; i < completeHtml.Length; i++)
            {
                state.Current = completeHtml[i];
                state.Next = ((i + 1) >= completeHtml.Length) ? ' ' : completeHtml[i + 1];

                if (!ProcessCharacter(state, onSummaryEnds))
                {
                    break;
                }

            }
            return state.All.ToString().Trim();
        }

        private static bool DefaultSummaryEndsFunc(IStringIterationState state)
        {
            switch (state.PartType)
            {
                case PartType.Letter:
                    return state.LetterCount >= state.NumberParts;
                case PartType.Word:
                    return state.WordCount >= state.NumberParts;
                case PartType.Sentance:
                    return state.SentanceCount >= state.NumberParts;
                case PartType.ClosedHtmlTag:
                    return state.TagCount >= state.NumberParts;
                case PartType.ClosedDivTag:
                    return state.DivTagCount >= state.NumberParts;
                case PartType.ClosedPTag:
                    return state.PTagCount >= state.NumberParts;
                case PartType.ClosedDivTagOrPTag:
                    return state.PAndDivTagCount >= state.NumberParts;
            }
            return false;
        }

        private static bool ProcessCharacter(StringIterationState state, Func<IStringIterationState, bool> summaryEnds)
        {
            // start tag begins
            if (state.Current == '<' && IsAsciiLetter(state.Next))
            {
                StartHtmlTagBegins(state);
            }
            // if this is the '/' character of the end tag
            else if (state.IsWithinHtmlEndTag && state.Current == '/')
            {
                return true;
            }
            // end tag begins
            else if (state.Current == '<' && state.Next == '/')
            {
                EndHtmlTagBegins(state);
            }
            // is within a tag
            else if (state.IsWithinHtmlStartTag || state.IsWithinHtmlEndTag)
            {
                // the tag name has ended tag attributes begin
                if (state.CurrentTagAttributes == null && state.Current != '>' && !IsAsciiLetterOrNumber(state.Current))
                {
                    state.CurrentTagAttributes = new StringBuilder();
                }
                WithinHtmlTag(state);
            }
            else // if is not within HTML tag
            {
                if (summaryEnds(state))
                {
                    FinalizeState(state);
                    return false;
                }
                OutsideOfHtmlTag(state);
            }
            return true;
        }

        private static void AppendStartTag(IStringIterationState state)
        {
            var tag = state.TagsStack.Peek();
            state.All.Append('<');
            state.All.Append(tag);
            if (state.CurrentTagAttributes != null)
            {
                state.All.Append(state.CurrentTagAttributes.ToString());
            }
            state.All.Append('>');
        }

        private static void AppendAutoCloseTag(string tag, IStringIterationState state)
        {
            state.All.Append('<');
            state.All.Append(tag);
            var attrEndWithSlash = false;
            if (state.CurrentTagAttributes != null)
            {
                var attr = state.CurrentTagAttributes.ToString();
                state.All.Append(attr);
                if (attr.EndsWith("/"))
                {
                    attrEndWithSlash = true;
                }
            }
            if (!attrEndWithSlash)
            {
                state.All.Append(' ');
                state.All.Append('/');
            }
            state.All.Append('>');
        }

        private static void AppendAndRemoveFromStackTheEndTag(IStringIterationState state)
        {
            var tag = state.TagsStack.Pop();

            state.All.Append('<');
            state.All.Append('/');
            state.All.Append(tag);
            state.All.Append('>');
        }

        private static void FinalizeState(IStringIterationState state)
        {
            while (state.TagsStack.Count > 0)
            {
                AppendAndRemoveFromStackTheEndTag(state);
            }
        }

        private static void StartHtmlTagBegins(StringIterationState state)
        {
            state.IsWithinHtmlStartTag = true;
            state.CurrentTag = new StringBuilder();
        }
        private static void EndHtmlTagBegins(StringIterationState state)
        {
            state.IsWithinHtmlEndTag = true;
            state.CurrentTag = new StringBuilder();
        }
        private static void HtmlTagEnds(StringIterationState state)
        {
            var tag = state.CurrentTag.ToString().ToLower();
            if (state.IsWithinHtmlStartTag)
            {
                if (AutoCloseTags.Contains(tag))
                {
                    AppendAutoCloseTag(tag, state);
                    MarkEndOfAreaWithinHtmlTag(state);
                    state.TagCount++;
                }
                else
                {
                    state.TagsStack.Push(tag);
                    AppendStartTag(state);
                }

            }
            // the end of the html end tag
            else if (/*isWithinHtmlEndTag &&*/ state.TagsStack.Count > 0 && state.TagsStack.Peek() == tag)
            {
                state.TagCount++;
                if (tag == "p")
                {
                    state.PTagCount++;
                }
                else if (tag == "div")
                {
                    state.DivTagCount++;
                }

                AppendAndRemoveFromStackTheEndTag(state);

            }
            MarkEndOfAreaWithinHtmlTag(state);
        }

        private static void MarkEndOfAreaWithinHtmlTag(StringIterationState state)
        {
            state.IsWithinHtmlStartTag = false;
            state.IsWithinHtmlEndTag = false;
            state.CurrentTag = null;
            state.CurrentTagAttributes = null;
        }



        private static void WithinHtmlTag(StringIterationState state)
        {
            // if this is the end of the area within a tag
            if (state.Current == '>')
            {
                HtmlTagEnds(state);
            }
            // the tag has ended, we are within the tag attributes area
            else if (state.CurrentTagAttributes != null)
            {
                state.CurrentTagAttributes.Append(state.Current);
            }
            else // if currentTagAttributes == null
            {
                state.CurrentTag.Append(state.Current);
            }
        }



        private static void OutsideOfHtmlTag(StringIterationState state)
        {
            if (SentanceLimiters.Contains(state.Current) && !SentanceLimiters.Contains(state.Next))
            {
                state.SentanceCount++;
                state.WordCount++;
                state.LetterCount++;
                state.IsWithinSpace = true;
            }
            else if (!state.IsWithinSpace && Spaces.Contains(state.Current))
            {
                state.WordCount++;
                state.IsWithinSpace = true;
            }
            else if (!Spaces.Contains(state.Current))
            {
                state.LetterCount++;
                state.IsWithinSpace = false;
            }
            state.All.Append(state.Current);
        }

        internal class StringIterationState : IStringIterationState
        {
            public char Current { get; internal set; }
            public char Next { get; internal set; }
            public bool IsWithinHtmlStartTag { get; internal set; }
            public bool IsWithinHtmlEndTag { get; internal set; }
            public bool IsWithinSpace { get; internal set; }
            public StringBuilder CurrentTag { get; internal set; }
            public StringBuilder CurrentTagAttributes { get; internal set; }
            public Stack<string> TagsStack { get; internal set; }
            public StringBuilder All { get; internal set; }
            public int LetterCount { get; internal set; }
            public int WordCount { get; internal set; }
            public int SentanceCount { get; internal set; }
            public int TagCount { get; internal set; }
            public int PTagCount { get; internal set; }
            public int DivTagCount { get; internal set; }
            public int PAndDivTagCount { get { return this.PTagCount + this.DivTagCount; } }
            public int NumberParts { get; internal set; }
            public PartType PartType { get; internal set; }

            public StringIterationState()
            {
                this.TagsStack = new Stack<string>();
                this.All = new StringBuilder();
                this.IsWithinSpace = true;
            }
        }

        private static bool IsAsciiLetter(char ch)
        {
            return AsciiLetters.Contains(ch);
        }

        private static bool IsAsciiLetterOrNumber(char ch)
        {
            return AsciiLettersAndNumbers.Contains(ch);
        }
    }
}
